#!/usr/bin/python
'''
/*
 * This computer program is the confidential information and proprietary trade
 * secret of VistaraIT LLC.  Possessions and use of this program must  conform
 * strictly to the license agreement between the user and VistaraIT LLC.,  and
 * receipt or possession does not convey any rights to divulge, reproduce,  or
 * allow others to use this program without specific written authorization  of
 * VistaraIT LLC.
 * 
 * Copyright (c) 2014 VistaraIT, Inc. All rights reserved.
 */
'''

from criticalsectionlock import CriticalSectionLock
from django.template.loader import render_to_string
import os, re, time, base64, socket
import dateutils
from xmlreader import XMLReader
from commons import executeCommand
import applicationproperties as properties
import logger

class TunnelManager():
 
    SA_free_port_start_range = 5000
    SA_free_port_end_range = 65535
    tunnel_success_retry_count = 3
    connect_timeout = '30'
    server_alive_interval = '30'
    server_alive_countMax = '3'

    tunnel_key = ''
    tunnel_temp_file = ''
    tunnelKey = ''

    sa_free_port = {}
    tunnelConfArray = {}
    tunnelStatusArray = {}
    servicesArray = []
    
    def __init__(self,appmanager):
        self.appmanager = appmanager
        self.tunnel_key = properties.TUNNEL_SSH_KEY
        self.tunnel_temp_file = properties.TUNNEL_TEMP_FILE

    def establishTunnel(self):
        if self.appmanager.isRegistered() == False:
            logger.logMessage(logger.INFO, 'Tunnel', 'Appliance is not yet registered.')
            return False, "Appliance is not yet registered."

        retval = False
        logger.logMessage(logger.INFO, 'Tunnel Request', 'Initiating Tunnel Check Request.')
        try:
            lock = CriticalSectionLock(properties.TUNNEL_LOCK_FILE)
            lock.acquire()
        except IOError as errMsg:
            logger.logMessage(logger.INFO, 'Tunnel Request', 'A Tunnel request is already in process. Ignoring the current request.' + str(errMsg))
            return retval, "A request to establish the tunnel is already in process. Ignoring the current request"

        try:
            retval, status = self.monitorTunnel()
        except Exception as err:
            logger.logMessage(logger.INFO, 'Tunnel Request Exception', str(err))
            status = str(err)

        if not lock.release():
            logger.logMessage(logger.INFO, 'Tunnel Request', "Failed to Release the acquired lock on tunnel lock file")
        if retval:
            logger.logMessage(logger.INFO, 'Tunnel Running Status', str(retval))
        return retval, status

    '''
    Method to get tunnel grep command
    '''
    def tunnel_grep_command(self):
        return properties.SSH_PATH + ' -nNT -f -o BatchMode=yes -o StrictHostKeyChecking=no'

    def monitorTunnel(self):

        tunnelRunning = self.isTunnelRunning()
        if tunnelRunning:
            return True, "Tunnel is already running."

        # Establish tunnel
        new_key_request = False
        if not os.path.exists(self.tunnel_key):
            new_key_request = True
        if self.getCSNDetails(new_key_request) == False:
            logger.logMessage(logger.ERROR, 'Tunnel', 'Failed to connect to information server.')
            return False, "Failed to retrieve connection server detail"
        
        self.getServiceDetails()
        ssh_server_port = self.tunnelConfArray['csn_rpport']
        ssh_tunnel_port = self.tunnelConfArray['csn_sshdport']
        ssh_server_ip = self.tunnelConfArray['csn_ip']
        ssh_user = self.tunnelConfArray['csn_tuser']

        if ssh_server_port != "" and ssh_tunnel_port != "" and ssh_server_ip != "" and ssh_user != "":
            tunnelStatus, error = self.invoke_tunnel_command()
            if tunnelStatus == 0:
                logger.logMessage(logger.INFO, 'Tunnel', 'Appliance tunnel is established.')
                if self.update_tunnel_status_at_info() == False:
                    self.kill_tunnels()
                    logger.logMessage(logger.ERROR, 'Tunnel', 'Failed to send success message to Information server. So killing tunnel..')
                    return False, "Failed to establish the tunnel"
                self.updateServiceConf()
                self.appmanager.cfgmanager.updateTunnelInfo('success', dateutils.getCurrentTimeAsString(), ssh_server_ip, ssh_tunnel_port)
                return True, "Tunnel has been successfully established"
            elif tunnelStatus == 1:
                logger.logMessage(logger.ERROR, 'Tunnel', 'Failed to establish tunnel due to remote port forwarding failed for listen.')
                self.appmanager.cfgmanager.updateTunnelInfo('port_error', dateutils.getCurrentTimeAsString(), ssh_server_ip, ssh_tunnel_port)
                return False, "Failed to establish the tunnel"
            elif tunnelStatus == 2:
                logger.logMessage(logger.ERROR, 'Tunnel', 'Failed to establish tunnel due to - Permission denied by SCC server.')
                if os.path.exists(self.tunnel_key):
                    os.remove(self.tunnel_key)
                self.appmanager.cfgmanager.updateTunnelInfo('key_error', dateutils.getCurrentTimeAsString(), ssh_server_ip, ssh_tunnel_port)
                return False, "Failed to establish the tunnel"
            else:
                logger.logMessage(logger.ERROR, 'Tunnel', 'Failed to establish tunnel due to - ' + error)
                self.appmanager.cfgmanager.updateTunnelInfo('tunnel_error', dateutils.getCurrentTimeAsString(), ssh_server_ip, ssh_tunnel_port)
                return False, "Failed to establish the tunnel"
        else:
            logger.logMessage(logger.ERROR, 'Tunnel', 'Failed to establish tunnel due to insufficient details from information server.')
            return False, "Failed to establish tunnel due to insufficient details from cloud server"

    '''
    Send Free Port Request
    '''
    def getCSNDetails(self, keyRequired):
        statusArray = {}
        auth_token = self.appmanager.getRegistrationInfo()['auth_token']
        #logger.logMessage(logger.INFO, 'Tunnel Port', str(self.appmanager.getRegistrationInfo()))
        if keyRequired:
            statusArray['tunnel_key'] = "true"
        tunnelData = self.createTunnelRequestData(properties.TUNNEL_ACTION_GETPORTS, auth_token, statusArray)
        logger.logMessage(logger.INFO, 'TunnelRequest', tunnelData)
        
        url = self.appmanager.toUpdateServerURL(properties.URL_TUNNEL_REQ)
        try:
            status, response = self.appmanager.postXMLData(url,tunnelData)
        except Exception as (errno, errmsg):
            if errno == 111:
                logger.logMessage(logger.INFO, 'Connection Exception', str(errno) + " - " + str(errmsg))
            elif errno == 101:
                logger.logMessage(logger.INFO, 'Network Exception', str(errno) + " - " + str(errmsg))
            else:
                logger.logMessage(logger.INFO, 'Caught Exception', str(errno) + " - " + str(errmsg))
            return False

        logger.logMessage(logger.INFO, 'TunnelResponse', str(status) + " " + str(response))
        if status != 200:
            return False
        
        if self.parseTunnelResponse(response):
            if keyRequired and self.tunnelKey != "":
                self.updateTunnelKey(self.tunnelKey)
            return True
        else:
            logger.logMessage(logger.ERROR, 'TunnelCommunication', 'Failed to connect to cloud server.')
            return False
        return True
    
    def createTunnelRequestData(self,action,auth_token,paramArray):
        try:
            hwInfo = self.appmanager.readFileProperties(properties.NSG_HW_INFO_FILE)
            model = hwInfo['APPLIANCE_MODEL']
        except:
            model = properties.UNKNOWN_MODEL_TAG
        #logger.logMessage(logger.ERROR, 'TunnelCommunication', str(auth_token))
        release = executeCommand("/usr/bin/sudo /usr/bin/lsb_release -rs 2>/dev/null", [], True)[1]
        return render_to_string('config/tunnelrequest.xml', {'uuid':auth_token,'action':action,'params':paramArray,'model':model,'release':release})


    def kill_tunnels(self):
        if not self.isTunnelRunning():
            return True
        
        kill_cmd = "/usr/bin/sudo /bin/kill -9 `pgrep -f '" + self.tunnel_grep_command() + "'`";
        rem_lockfile_cmd = "/usr/bin/sudo /bin/rm -f " + properties.TUNNEL_LOCK_FILE
            
        result = executeCommand(kill_cmd, [], True)
        if result[0] != 0:
            return False
        else:
            executeCommand(rem_lockfile_cmd,[],True)
            return True

        #self.appmanager.cfgmanager.updateTunnelInfo()
        
    # Method to enable SSH tunnel 
    def invoke_tunnel_command(self):
        tunnel_cmd = self.tunnel_command()
        #logger.logMessage(logger.ERROR, 'Tunnel Command', str(tunnel_cmd))
        result = executeCommand(tunnel_cmd, [], True)
        cmd1 = "/usr/bin/sudo chown admin.admin " + self.tunnel_temp_file + " >/dev/null 2>&1"    
        executeCommand(cmd1, [], True)
        if result[0] != 0:
            cmd = "grep 'Permission denied' " + self.tunnel_temp_file
            result = executeCommand(cmd, [], True)
            if result[0] == 0:
                return 2, result[1]

            cmd = 'cat ' + self.tunnel_temp_file
            result = executeCommand(cmd, [], True)
            return 3, result[1]
        else:
            time.sleep(5)
            # If CSN Tunnel port is already in use
            cmd = "grep 'Warning: remote port forwarding failed for listen port' " + self.tunnel_temp_file
            result = executeCommand(cmd, [], True)
            if result[0] == 0:
                if self.kill_tunnels() == False:
                    logger.logMessage(logger.ERROR, 'Tunnel', 'Failed to kill the previous tunnnels.')
                return 1, result[1]
            
            # If one of the Local Tunnel ports is already in use
            cmd = "grep 'channel_setup_fwd_listener: cannot listen to port' " + self.tunnel_temp_file
            result = executeCommand(cmd, [], True)
            if result[0] == 0:
                if self.kill_tunnels() == False:
                    logger.logMessage(logger.ERROR, 'Tunnel', 'Failed to kill the previous tunnnels.')
                return 1, result[1]
        return 0, "Success"
    
    # Method to get tunnel command 
    def tunnel_command(self):
        port_redir_str = ''
        service_count = len(self.servicesArray)
        saPortArray = self.getSAFreePort(service_count)
        sa_port_count = len(saPortArray)
        
        if sa_port_count == service_count:
            serCount = 0
            self.servicesArray.reverse()
            for service in self.servicesArray:
                lport = saPortArray[serCount]
                self.sa_free_port[service] = lport
                port_redir_str = port_redir_str +  " -L " + str(lport) + ":" + self.tunnelConfArray[service + "_ip"] + ":" + self.tunnelConfArray[service + "_rport"]
                serCount = serCount + 1

            port_redir_str += " -L *:8443:127.0.0.1:8443 -L *:443:127.0.0.1:443 "
            conn_type = self.appmanager.cfgmanager.getProxyProperties(self.appmanager.cfgmanager.NETWORK_CON_TYPE)
            if conn_type == "direct":
                self.tunnelStatusArray['proxy_enabled'] = False
                tunnel_command = "/usr/bin/sudo " + self.tunnel_grep_command() + " -o ServerAliveCountMax=" + self.server_alive_countMax + " -o ServerAliveInterval=" + self.server_alive_interval + " -o ConnectTimeout=" + self.connect_timeout + " -i " + self.tunnel_key + " -o UserKnownHostsFile=/dev/null " + port_redir_str + " -p " + self.tunnelConfArray['csn_sshdport'] + " -d " + self.tunnelConfArray['csn_rpport'] + " " + self.tunnelConfArray['csn_tuser'] + "@" + self.tunnelConfArray['csn_ip'] + " >" + self.tunnel_temp_file + " 2>&1"
            else:
                self.tunnelStatusArray['proxy_enabled'] = True
                proxy_ip = self.appmanager.cfgmanager.getProxyProperties(self.appmanager.cfgmanager.PROXY_IP)
                proxy_port = self.appmanager.cfgmanager.getProxyProperties(self.appmanager.cfgmanager.PROXY_PORT)
                proxy_usr = self.appmanager.cfgmanager.getProxyProperties(self.appmanager.cfgmanager.PROXY_USR)
                proxy_pwd = self.appmanager.cfgmanager.getProxyProperties(self.appmanager.cfgmanager.PROXY_PWD)
                if proxy_usr != "" and proxy_pwd != "":
                    os.putenv("HTTP_PROXY_PASSWORD", proxy_pwd)
                    proxy_cmd = " -o ProxyCommand=\'/usr/bin/connect -H " + proxy_usr + "@" + proxy_ip + ":" + proxy_port + " %h %p\'"
                else:
                    proxy_cmd = " -o ProxyCommand=\'/usr/bin/connect -H " + proxy_ip + ":" + proxy_port + " %h %p\'"
                #proxy_cmd = " -o ProxyCommand=\'/bin/nc -X connect -x " + proxy_ip + ":" + proxy_port + " %h %p\'"
                tunnel_command = "/usr/bin/sudo " + self.tunnel_grep_command() + " -o ServerAliveCountMax=" + self.server_alive_countMax + " -o ServerAliveInterval=" + self.server_alive_interval + " -o ConnectTimeout=" + self.connect_timeout + " -i " + self.tunnel_key + " -o UserKnownHostsFile=/dev/null " + port_redir_str + proxy_cmd + " -p " + self.tunnelConfArray['csn_sshdport'] + " -d " + self.tunnelConfArray['csn_rpport'] + " " + self.tunnelConfArray['csn_tuser'] + "@" + self.tunnelConfArray['csn_ip'] + " >" + self.tunnel_temp_file + " 2>&1"
            return tunnel_command 
        else:
            raise Exception('Failed to get free ports at SA.')
    
    
    def isTunnelRunning(self):
        cmd = "ps -ef | grep '" + self.tunnel_grep_command() + "' | grep -v grep"
        result = executeCommand(cmd, [], True)
        return result[0] == 0
    
    def getTunnelInfo(self):
        value = {}
        is_tunnel = self.isTunnelRunning()
        value['is_tunnel'] = is_tunnel
        value['tunnel_status'] = "Established" if is_tunnel else 'Not Established'
        return value
    
    # Method to get free ports from range to establish tunnel 
    def getSAFreePort(self,serviceCnt):
        count = 0;
        portArray = []
        for port in range(self.SA_free_port_start_range, self.SA_free_port_end_range):
            if count == serviceCnt:
                break
            if self.checkSAFreePort(port) == True:
                portArray.append(port)
                count += 1
        return portArray
    
    # Check free port
    def checkSAFreePort(self,port):
        host = "127.0.0.1"
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.bind((host, port))
            sock.close()
            del sock
            return True
        except:
            return False
       
    # Method to get service details
    def getServiceDetails(self):
        for key,value in self.tunnelConfArray.iteritems():
            if re.match('^service_name', key):
                self.servicesArray.append(value)
    
    #function to update temporary key file
    def updateTunnelKey(self, tunnelKey):
        try:
            fout = open(self.tunnel_key, "wb")
            fout.write(tunnelKey)
            fout.close()
            cmd1 = "/usr/bin/sudo chown admin.admin " + self.tunnel_key + " >/dev/null 2>&1"    
            executeCommand(cmd1, [], True)
            cmd2 = "/usr/bin/sudo chmod 600 " + self.tunnel_key + " >/dev/null 2>&1"    
            executeCommand(cmd2, [], True)
        except:
            pass
        
    # Function to prepare hash file for tunnel_status to information server
    def prepare_tunnel_status_to_info(self):
        self.tunnelStatusArray['csn_id'] = self.tunnelConfArray['csn_id']
        self.tunnelStatusArray['csn_rpport'] = self.tunnelConfArray['csn_rpport']
        for service in self.servicesArray:
            self.tunnelStatusArray[service + '_sport'] =  self.tunnelConfArray[service + '_rport']
    
    # Function to update Information with success tunnel status
    def update_tunnel_status_at_info(self):
        try:
            self.prepare_tunnel_status_to_info()
            i = 0
            while i < self.tunnel_success_retry_count:
                i += 1
                if self.sendTunnelSuccess() == True:
                    return True
            return False
        except:
            return False
        
    #Send tunnel success message
    def sendTunnelSuccess(self):
        try:
            # Sending present IP address
            networkInfo = self.appmanager.getNetworkInfo()
            ipaddress = networkInfo['ipaddr']
            self.tunnelStatusArray['ip_address'] = ipaddress
            self.tunnelStatusArray['sw_version'] = self.appmanager.getVersionInfo()['appliance']
            
            # Get SAID for communication
            auth_token = self.appmanager.getRegistrationInfo()['auth_token']
            
            '''
            auth_token=""
            
            if os.path.exists(properties.LB_PROPERTY_FILE):
                lines = open(properties.LB_PROPERTY_FILE)
                for line in lines:
                    keyValues = line.split("=")
                    if keyValues[0] == "nda.auth.token":
                        auth_token = keyValues[1].rstrip()
                self.tunnelStatusArray['nda_auth_token'] = auth_token
            '''
            
            '''
            try:
                cmd = "/bin/uname -r | grep xen"
                if executeCommand(cmd, [], True)[0] == 0:
                    logger.logMessage(logger.INFO, 'Tunnel', 'Found virtualization')
                    cmd = "/usr/bin/sudo /usr/bin/virsh list --all | grep " + properties.BDRCORE_DOMAIN_NAME
                    result = executeCommand(cmd, [], True)
                    if result[0] == 0:
                        logger.logMessage(logger.INFO, 'Tunnel', 'Found BDR core VM')
                        coreParams = self.appmanager.getCoreDetails()
                        for key,value in coreParams.iteritems():
                            self.tunnelStatusArray[key] = value
                    else:
                        logger.logMessage(logger.INFO, 'Tunnel', 'Could not found BDR core VM')
            except:
                logger.logMessage(logger.INFO, 'Tunnel', 'Exception Caught while getting BDR core details')
                pass
            '''
            
            try:
                debugMsg = ""
                debugInfoFile = self.appmanager.cfgmanager.getProperty(self.appmanager.cfgmanager.DEBUG_INFO_FILE)
                if os.path.exists(debugInfoFile):
                    cmd = "/bin/cat " +  debugInfoFile
                    debugMsg = executeCommand(cmd, [], True)[1].strip()
    
                self.tunnelStatusArray["debug_info"] = debugMsg
                cmd = "/usr/bin/sudo /bin/rm -f " + debugInfoFile
                executeCommand(cmd, [], True)
            except:
                logger.logMessage(logger.INFO, 'Tunnel', 'Exception Caught while getting debug information')
                pass
    
            tunnelSuccessData = self.createTunnelRequestData(properties.TUNNEL_ACTION_GETSUCCESS,auth_token,self.tunnelStatusArray)
            logger.logMessage(logger.INFO, 'Tunnel Success Msg', tunnelSuccessData)
            
            url = self.appmanager.toUpdateServerURL(properties.URL_TUNNEL_REQ)
            try:
                status, response = self.appmanager.postXMLData(url,tunnelSuccessData)
            except Exception as (errno,errmsg):
                if errno == 111:
                    logger.logMessage(logger.INFO, 'Connection Exception', str(errno) + " - " + str(errmsg))
                elif errno == 101:
                    logger.logMessage(logger.INFO, 'Network Exception', str(errno) + " - " + str(errmsg))
                else:
                    logger.logMessage(logger.INFO, 'Caught Exception', str(errno) + " - " + str(errmsg))
                return False
            
            logger.logMessage(logger.INFO, 'Tunnel Success Response', str(status) + " " + str(response))
            if status != 200:
                return False
    
            if self.parseTunnelResponse(response):
                logger.logMessage(logger.INFO, 'Tunnel', 'Successfully updated tunnel success message.')
                return True
            else:
                logger.logMessage(logger.ERROR, 'Tunnel', 'Failed to communication with SCC server.')
                return False
        except:
            return False
        
    def parseTunnelResponse(self, tunnelResponse):
        xmlreader = XMLReader(tunnelResponse)
        thash = xmlreader.processData('res')
        
        version = thash['res.ver']
        if version == "1.0":
            action = ''
            action = thash['res.action']
            
            if action == "GetTunnelPorts":
                self.tunnelConfArray['action'] = action
                return self.parseType1Response(thash)
            elif action == "SAEvent" or action == "SADeregistrationRequest":
                return self.parseType2Response(thash)
            else:
                logger.logMessage(logger.ERROR, 'Tunnel', 'This message type is not supported.')
                return False
        else:
            logger.logMessage(logger.ERROR, 'Tunnel', 'This version messages are not supported.')
            return False
        
    def parseType1Response(self, thash):
        res_status = thash['res.param.resstatus.state']
        if res_status != "success":
            errorCode = thash['res.param.resstatus.error.errorcode']
            if errorCode == "3" or errorCode == "4":
                self.appmanager.unregisterAppliance()
            logger.logMessage(logger.ERROR, 'Tunnel Communication', "Tunnel request failed with error message: " + thash['res.param.resstatus.error.errormsg'])
            return False
        
        reg_status = thash['res.param.regstatus.state']
        if reg_status != 'reg':
            logger.logMessage(logger.ERROR, 'Tunnel Communication', "Invalid Appliance: " + reg_status)
            return False
        
        self.tunnelConfArray['csn_id'] = thash['res.param.csn.id']
        self.tunnelConfArray['csn_ip'] = thash['res.param.csn.ip']
        self.tunnelConfArray['csn_sshdport'] = thash['res.param.csn.sshdport']
        self.tunnelConfArray['csn_rpport'] = thash['res.param.csn.rpport']
        self.tunnelConfArray['csn_tuser'] = thash['res.param.csn.tuser']
        TKeyValue = ""
        try:
            TKeyValue = thash['res.param.csn.tkey']
        except:
            pass
        if TKeyValue != "":
            self.tunnelKey = base64.decodestring(TKeyValue)
                
        service_cnt = len(thash['res.param.services'])
        for i in range(0, service_cnt):
            serviceName = thash['res.param.services'][i]['name']
            self.tunnelConfArray["service_name." + serviceName] = serviceName
            self.tunnelConfArray[serviceName + "_ip"] = thash['res.param.services'][i]['ip']
            self.tunnelConfArray[serviceName + "_rport"] = thash['res.param.services'][i]['port']
            
        return True

    def parseType2Response(self, thash):
        res_status = thash['res.param.resstatus.state']
        if res_status != "success":
            logger.logMessage(logger.ERROR, 'Tunnel Communication', "Tunnel request failed with error message: " + thash['res.param.resstatus.error.errormsg'])
            return False
        return True    
         
    # Method to update configuration files
    def updateServiceConf(self):
        for key, value in self.sa_free_port.iteritems():
            logger.logMessage(logger.INFO, 'Tunnel Communication', "Service: " + key + ",  Port: " + str(value))
            if key == 'MSG':
                continue
            elif key == 'NEPROBE' or key == 'DISCO':
                neprobe_port = value
                msg_port = self.sa_free_port.get('MSG', '')
                value = {'NEPROBE': neprobe_port, 'MSG': msg_port}
            
            if self.appmanager.generateConfig(key, value):
                logger.logMessage(logger.INFO, 'Tunnel Communication', "Successfully updated configuration with SA free port for service: " + key)
            else:
                logger.logMessage(logger.ERROR, 'Tunnel Communication', "Failed to update configuration with SA free port for service: " + key)

    
